package com.gitee.maskit.web;

import com.gitee.maskit.utils.Response;
import com.gitee.maskit.utils.ResponseAdapter;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.springframework.context.MessageSource;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @author z0228
 */
@Order(Ordered.HIGHEST_PRECEDENCE)
public class ExceptionResolver extends ExceptionHandlerExceptionResolver {
    private static final ModelAndView HANDLED = new ModelAndView();
    private static final LoadingCache<MaskitWebProperties, Map<Class, Integer>> STATUS =
        CacheBuilder.newBuilder().expireAfterWrite(3, TimeUnit.MINUTES)
            .build(new CacheLoader<MaskitWebProperties, Map<Class, Integer>>() {
                @Override
                public Map<Class, Integer> load(MaskitWebProperties properties) throws Exception {
                    final Map<Class, Integer> statuses = new HashMap<>();
                    if (null != properties.getException().getStatuses()) {
                        properties.getException().getStatuses().forEach((status, classes) -> {
                            if (null != classes) {
                                classes.forEach(c -> statuses.put(c, status));
                            }
                        });
                    }
                    return statuses;
                }
            });
    private static final LoadingCache<MaskitWebProperties, Set<Class>> IGNORE_CAUSE =
        CacheBuilder.newBuilder().expireAfterWrite(3, TimeUnit.MINUTES)
            .build(new CacheLoader<MaskitWebProperties, Set<Class>>() {
                @Override
                public Set<Class> load(MaskitWebProperties properties) throws Exception {
                    if (null != properties.getException().getIgnoreCauses()) {
                        return new HashSet<>(properties.getException().getIgnoreCauses());
                    } else {
                        return Collections.emptySet();
                    }
                }
            });
    private static final Map<Class, ResponseBuilder> BUILDERS = new HashMap<>();
    private final boolean forceHandle;
    private final ResponseAdapter<Object> adapter;
    private final MaskitWebProperties properties;
    private final MessageSource source;
    private final MappingJackson2HttpMessageConverter converter;

    public ExceptionResolver(boolean forceHandle, ResponseAdapter<Object> adapter, MaskitWebProperties properties, MessageSource source, MappingJackson2HttpMessageConverter converter, List<ResponseBuilder> builders) {
        this.forceHandle = forceHandle;
        this.adapter = adapter;
        this.properties = properties;
        this.source = source;
        this.converter = converter;
        if (null != builders) {
            builders.forEach(builder -> builder.capability().forEach(c -> BUILDERS.put(c, builder)));
        }
    }

    @Override
    protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod, Exception exception) {
        final boolean shouldHandle = null != exception &&
            (forceHandle || Headers.XRequestedWith.match(request)) &&
            (null == exception.getCause() ||
                !IGNORE_CAUSE.getUnchecked(properties)
                    .contains(exception.getCause().getClass()));
        if (shouldHandle) {
            final Class exceptionClass = exception.getClass();
            final ServletServerHttpResponse servletServerHttpResponse =
                new ServletServerHttpResponse(response);
            response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
            try {
                response.setStatus(
                    STATUS.getUnchecked(properties).getOrDefault(exceptionClass,
                        Response.GENERAL_FAILURE_STATUS));

                Response result = build(request, response, handlerMethod, exception);

                if (null != source) {
                    result.setMessage(source.getMessage(
                        result.getError(), result.args(),
                        result.getError(), WebHolder.LOCALE.get()));
                } else {
                    result.setMessage(result.getError());
                }

                if (null != adapter) {
                    converter.write(adapter.apply(result),
                        MediaType.APPLICATION_JSON_UTF8,
                        servletServerHttpResponse);
                } else {
                    converter.write(result,
                        MediaType.APPLICATION_JSON_UTF8,
                        servletServerHttpResponse);
                }

                logger.info(exception);
                return HANDLED;
            } catch (IOException e) {
                logger.warn("Unable to response with " +
                    MediaType.APPLICATION_JSON_UTF8_VALUE + " for ", e);
            }
        }
        return super.doResolveHandlerMethodException(request, response, handlerMethod, exception);
    }

    private Response build(final HttpServletRequest request, final HttpServletResponse response, final HandlerMethod handlerMethod, final Exception exception) {
        Response result = null;
        if (BUILDERS.containsKey(exception.getClass())) {
            result = BUILDERS.get(exception.getClass()).build(request, response, handlerMethod, exception);
        }
        if (null == result) {
            return Response.no(response.getStatus(), exception.getMessage());
        } else {
            return result;
        }
    }

    private enum Headers {
        /**
         * AJAX request
         */
        XRequestedWith("X-Requested-With", "XMLHttpRequest");
        private final String header;
        private final String value;

        Headers(String header, String value) {
            this.header = header;
            this.value = value;
        }

        public boolean match(final HttpServletRequest request) {
            String header;
            return null != (header = request.getHeader(this.header)) && this.value.equalsIgnoreCase(header);
        }
    }
}
